/*
  Program: Mine Mayhem
  File:    times.cc
  Date:    July, 1996 to February,   1997,    version 1.00
	   26 October to 3 December,	      version 2.00
	   8 December to 4 January,  2001,    version 2.10
	   15 January,			      version 2.11
	   18 September to 16 December, 2006, version 2.90

  Everything to do with record times and statistics.

  Freeware, Copyright 2006 Jason Hood.
*/

#include <time.h>
#include <stdio.h>
#include <tws/message.h>
#include "times.h"
#include "minefld.h"
#include "timesicn.h"

extern BYTE HighLightColour;


enum
{
  ID_SCORETIME = 30001,
  ID_SCORE3BVPS,
  ID_SCORESTATS
};


double Layout::speed(double time) const
{
  return mines / time;
}

double Layout::rating(int bbbv) const
{
  return 100. * bbbv / (field->area(dim) - mines);
}

const char* Layout::str() const
{
  static char buf[24];
  sprintf(buf, "%d x %d, %d mines", dim.x, dim.y, mines);
  return buf;
}


void Table::Entry::clear()
{
  memset(this, 0, sizeof(*this));
  time = 1000000;		// ie. 1000.000, bigger than any player time
}


static Layout* get_layout(int level)
{
  Layout* lo;

  switch (level)
  {
    case BEGINNER:     lo = &field->Beginner;	  break;
    case INTERMEDIATE: lo = &field->Intermediate; break;
    case EXPERT:       lo = &field->Expert;	  break;
    default:	       lo = &field->Custom;	  break;
  }

  return lo;
}


// Convert the entry to a string.
char* Table::Entry::str(int lev, int score, bool opt) const
{
  static char buffer[100];
  static const char strstart[]	 = "NDB ";      // none, displace, blank (perimeter)
  static const char strfinish[]  = "BOME";      // both, open, mark, either
  static const char strcounter[] = "NRU";       // normal, remove, update
  static const char strmarks[]	 = "FC";        // flag, counter
  static const char strtrack[]	 = "IT";        // individual, tracking
  static const char strclear[]	 = "MA";        // manual, automatic
  static const char strwrap[]	 = "FW";        // flat, wrap

  if (*date == '\0')
  {
    *buffer = '\0';
  }
  else
  {
    double t = time / 1000.;
    char bps[8];
    // Some games may have abnormally large 3BV/s values, so adjust the
    // decimals to keep it the right width.
    sprintf(bps, "%7.*f", bbbv/t < 1000 ? 3 : 0, bbbv/t);

    int len;
    Layout* lo = get_layout(lev);
    if (score == ID_SCORETIME)
      len = sprintf(buffer, "%7.3f  %7.3f  %s", t, lo->speed(t), bps);
    else // (score == ID_SCORE3BVPS)
      len = sprintf(buffer, "%s  %7.3f  %7.3f", bps, lo->speed(t), t);
    len += sprintf(buffer+len, "  %4u  %7.3f  %s",
			       bbbv, lo->rating(bbbv), date);

    if (opt)
    {
      buffer[len++] = ' ';
      buffer[len++] = ' ';
      buffer[len++] = strstart[start];
      buffer[len++] = strfinish[finish];
      buffer[len++] = strcounter[options & OCounter];
      buffer[len++] = strmarks[bool(options & OMarks)];
      buffer[len++] = strtrack[bool(options & OTrack)];
      buffer[len++] = strclear[bool(options & OClear)];
      buffer[len++] = strwrap[bool(options & OWrap)];
      buffer[len] = '\0';
    }
  }

  return buffer;
}


// Constructor for an entire times table.
void Table::clear()
{
  gamestotal = bbbvtotal = games = one = 0;
  ratingtotal = 0.0;
  for (int j = 0; j < NoTotals; ++j)
    total[j] = 0.0;
  current = longestwins = longestloss = 0;

  for (unsigned j = 0; j < Entries; j++)
  {
    fastest[0][j].clear();
    fastest[1][j].clear();
  }
}


#define bwrite(file, var) fwrite(&(var), sizeof(var), 1, file);
#define bread( file, var) fread( &(var), sizeof(var), 1, file);


// Write the table to file.
void Table::write(FILE* file) const
{
  bwrite(file, gamestotal);
  if (gamestotal == 0)
    return;

  bwrite(file, bbbvtotal);
  bwrite(file, ratingtotal);
  bwrite(file, current);
  bwrite(file, longestloss);

  bwrite(file, games);
  if (games == 0)
    return;

  bwrite(file, one);
  bwrite(file, total);
  bwrite(file, longestwins);
  bwrite(file, startdate);

  unsigned entries = MIN(games, Entries);
  for (unsigned j = 0; j < entries; ++j)
  {
    bwrite(file, fastest[0][j]);
    bwrite(file, fastest[1][j]);
  }
}


// Read the table from file.
void Table::read(FILE* file)
{
  bread(file, gamestotal);
  if (gamestotal == 0)
    return;

  bread(file, bbbvtotal);
  bread(file, ratingtotal);
  bread(file, current);
  bread(file, longestloss);

  bread(file, games);
  if (games == 0)
    return;

  bread(file, one);
  bread(file, total);
  bread(file, longestwins);
  bread(file, startdate);

  unsigned entries = MIN(games, Entries);
  for (unsigned j = 0; j < entries; ++j)
  {
    bread(file, fastest[0][j]);
    bread(file, fastest[1][j]);
  }
}


static const char* const savestr[3] = { "Time ", "3BV/s", "  Avg" };

// Save the table to a text file.
void Table::save(FILE* file, const char* title, int lev) const
{
  if (file == NULL || games == 0)
    return;

  if (title)
  {
    int len = fprintf(file, "%s\n", title);
    for (; len > 1; --len)
      putc('-', file);
    fputs("\n\n", file);
  }

  unsigned entries = MIN(games, Entries);
  for (int k = 0; k < 2; ++k)
  {
    fprintf(file, "%s\n\n", savestr[k]);
    for (unsigned j = 0; j < entries; ++j)
      fprintf(file, "%s\n", fastest[k][j].str(lev, k+ID_SCORETIME, true));
    if (games > 1)
      fprintf(file, "\n%s\n", str(lev, k+ID_SCORETIME));
    fputc('\n', file);
  }
  fputc('\n', file);
}


// Save just the fastest entry and the average to a text file.
void Table::best(FILE* file, const char* title, int lev) const
{
  if (games == 0)
    return;

  fprintf(file, "%s\n\n", title);
  for (int k = 0; k < 2; ++k)
  {
    fprintf(file, "%s: %s\n", savestr[k],
			      fastest[k][0].str(lev, k+ID_SCORETIME, true));
    if (games > 1)
      fprintf(file, "%s: %s\n", savestr[2], str(lev, k+ID_SCORETIME));
    fputc('\n', file);
  }
  fputc('\n', file);
}


/* -------------------------------   Enter   --------------------------------

  Update the statistics for when a game was lost.

*/

void Table::Enter(int bbbv)
{
  ++gamestotal;
  bbbvtotal += bbbv;
  ratingtotal += field->layout.rating(bbbv);

  if (current > 0)
  {
    if (current > longestwins)
      longestwins = current;
    current = 0;
  }
  --current;
  if (current < longestloss)
    longestloss = current;

}


/* -------------------------------   Enter   --------------------------------

  Update the number of games and total time for this table.  If time is a
  record insert it at the appropriate position.
  Return -1 if the time was not a record, otherwise the index of the time.

*/

int Table::Enter(unsigned time, int bbbv)
{
  unsigned timerecord, bpsrecord;

  // Don't count lucky single-click games.
  if (field->Opened() == 1 && field->Marked() == 0)
  {
    ++one;
    return -1;
  }

  ++games;
  ++gamestotal;
  bbbvtotal += bbbv;
  ratingtotal += field->layout.rating(bbbv);

  double t = time / 1000.;
  double bps = bbbv / t;
  total[TIME]	+= t;
  total[SPEED]	+= field->layout.speed(t);
  total[BBBVPS] += bps;
  total[BBBV]	+= bbbv;
  total[RATING] += field->layout.rating(bbbv);

  if (current < 0)
  {
    if (current < longestloss)
      longestloss = current;
    current = 0;
  }
  ++current;
  if (current > longestwins)
    longestwins = current;

  unsigned j;
  for (j = 0; j < Entries; j++)
  {
    if (time <= fastest[0][j].time)	 // Same time is regarded as better
    {					 //  to reflect the later date
      shift(0, j);
      Stats(0, j, time, bbbv);
      break;
    }
  }
  timerecord = j;

  for (j = 0; j < Entries; j++)
  {
    if (bps >= fastest[1][j].bbbv / (fastest[1][j].time / 1000.))
    {
      shift(1, j);
      Stats(1, j, time, bbbv);
      break;
    }
  }
  bpsrecord = j;

  return (timerecord == Entries && bpsrecord == Entries) ? ~0 :
	 timerecord | (bpsrecord << 16);
}

// Make room for the new entry.
void Table::shift(int which, unsigned from)
{
  // fastest[from+1..Entries-1] = fastest[from..Entries-2]
  for (unsigned j = Entries-1; j > from; j--)
  {
    fastest[which][j] = fastest[which][j-1];
  }
}

// Place the values in the entry.
void Table::Stats(int which, unsigned position, unsigned time, unsigned bbbv)
{
  fastest[which][position].time    = time;
  fastest[which][position].bbbv    = bbbv;
  fastest[which][position].start   = Start;
  fastest[which][position].finish  = Finish;
  fastest[which][position].options = Counter;
  if (isMarks)	   fastest[which][position].options |= OMarks;
  if (isAutoClear) fastest[which][position].options |= OClear;
  if (isTracked)   fastest[which][position].options |= OTrack;
  if (field->canWrap())
    fastest[which][position].options |= OWrap;

  GetDate(fastest[which][position]);
  if (games == 1)
    strcpy(startdate, fastest[which][position].date);
}

// Place the date in the entry as "dd-Mmm-yyyy" (eg. 15-Feb-1997).
void Table::GetDate(Entry& entry)
{
  time_t now;
  time(&now);
  strftime(entry.date, DateLen, "%e-%b-%Y", localtime(&now));
}


// Format the statistics as a string, according to which:
//   0 = winning average, including start date and games won
//   1 = winning average,
//   2 = best average,
//   3 = overall average.
char* Table::str(int lev, int score, int which) const
{
  static char buffer[80];

  if (games <= 1 && which == 0)
  {
    *buffer = '\0';
  }
  else if (which >= 3)
  {
    sprintf(buffer, "%s%4.0f  %7.3f",
		    which == 3 ? "                           " : "",
		    double(bbbvtotal) / gamestotal, ratingtotal / gamestotal);
  }
  else
  {
    double t[NoTotals];
    unsigned g;
    if (which == 2)
    {
      memset(t, 0, sizeof(t));
      g = MIN(games, Entries);
      int r = (score == ID_SCORE3BVPS);
      Layout* lo = get_layout(lev);
      for (unsigned j = 0; j < g; j++)
      {
	double tim = fastest[r][j].time / 1000.;
	t[0] += tim;
	t[1] += lo->speed(tim);
	t[2] += fastest[r][j].bbbv / tim;
	t[3] += fastest[r][j].bbbv;
	t[4] += lo->rating(fastest[r][j].bbbv);
      }
    }
    else
    {
      g = games;
      if (score == ID_SCORETIME || which == 1)
      {
	t[0] = total[TIME];
	t[2] = total[BBBVPS];
      }
      else // (score == ID_SCORE3BVPS)
      {
	t[0] = total[BBBVPS];
	t[2] = total[TIME];
      }
      t[1] = total[SPEED];
      t[3] = total[BBBV];
      t[4] = total[RATING];
    }

    int len = sprintf(buffer, "%7.3f  %7.3f  %7.3f  %4.0f  %7.3f",
		      t[0] / g, t[1] / g, t[2] / g, t[3] / g, t[4] / g);
    if (which == 0)
      sprintf(buffer+len, "  %s  %5d won", startdate, games);
  }
  return buffer;
}


/* -------------------------------   Times   -------------------------------- */


Times::~Times()
{
  CustomTimes* list;
  while (head != NULL)
  {
    list = head;
    head = head->next;
    delete list;
  }
}


/* --------------------------------   Add   ---------------------------------

  Add the field's custom layout to the list.

*/

void Times::Add(cLayout& lo)
{
  if (!field->CustomPopup.Insert(lo))
    return;

  prevCustom = new CustomTimes;
  prevCustom->layout = lo;
  prevCustom->next = head;
  head = prevCustom;
}


/* -----------------------------   FindTable   ------------------------------

  Given the level, type of game and automatic options, return a pointer to
  the appropriate table.

*/

Table* Times::FindTable(int level, bool perimeter, bool automark, bool autoopen)
{
  TTable* table;

  switch (level)
  {
    case BEGINNER:     table = &Beginner;     break;
    case INTERMEDIATE: table = &Intermediate; break;
    case EXPERT:       table = &Expert;       break;
    default: if (prevCustom && field->Custom == prevCustom->layout)
	       table = &prevCustom->times;
             else
             {
	       for (prevCustom = head;; prevCustom = prevCustom->next)
	       {
		 if (prevCustom == NULL)
		   return NULL;
		 if (prevCustom->layout == field->Custom)
                 {
		   table = &prevCustom->times;
                   break;
                 }
               }
             }
             break;
  }
  return &(*table)[perimeter][autoopen*2 + automark];
}


/* -------------------------------   Check   --------------------------------

  Determine if time is a record for level, provided it's not a random game,
  or it's not automatic opening AND marking.  If it is a record, display it
  and return true.

*/

bool Times::Check(int level, int bbbv, unsigned time)
{
  if (level < BEGINNER || (isAutoMark && isAutoOpen))
    return false;

  Table* table = FindTable(level);
  if (table == NULL)
    return false;

  if (!field->GameWon())
  {
    table->Enter(bbbv);
    return false;
  }

  int isRecord = table->Enter(time, bbbv);
  if (isRecord != -1)
    TimesWindow(*table, level, isRecord).Run();
  return (isRecord != -1);
}


/* ------------------------------   Display   -------------------------------

  Display the times for level.	If both automatics are on, turn them off,
  since there is no time for that option.  If there are no times for this
  layout use Beginner.

*/

void Times::Display(int level, int which)
{
  bool mark = isAutoMark,
       open = isAutoOpen;
  if (isAutoMark && isAutoOpen)
    isAutoMark = isAutoOpen = false;

  Table* table = FindTable(level);
  if (table == NULL)
    table = FindTable(level = BEGINNER);
  TimesWindow(*table, level, which).Run();

  isAutoMark = mark;
  isAutoOpen = open;
}


/* --------------------------------   Save   --------------------------------

  Write all the times to disk.
  Returns false if the file could not be created, true otherwise.

*/

void Times::write(const TTable& table, FILE* file) const
{
  for (int r = 0; r < Records; ++r)
    for (int j = 0; j < Tables; ++j)
      table[r][j].write(file);
}

bool Times::Save(const char* filename)
{
  FILE* file = fopen(filename, "wb");
  if (!file)
    return false;

  write(Beginner,     file);
  write(Intermediate, file);
  write(Expert,       file);

  for (CustomTimes* list = head; list; list = list->next)
  {
    bwrite(file, list->layout);
    write(list->times, file);
  }

  fclose(file);
  return true;
}


/* --------------------------------   Load   --------------------------------

  Read all the times from disk and create the custom games list and popup menu.
  Returns false if the file could not be opened, true otherwise.

*/

void Times::read(TTable& table, FILE* file)
{
  for (int r = 0; r < Records; ++r)
    for (int j = 0; j < Tables; ++j)
      table[r][j].read(file);
}

bool Times::Load(const char* filename, CustomPopupRes& custom)
{
  FILE* file = fopen(filename, "rb");
  if (!file)
    return false;

  read(Beginner,     file);
  read(Intermediate, file);
  read(Expert,	     file);

  Layout lo;
  for (;;)
  {
    bread(file, lo);
    if (feof(file))
      break;
    Add(lo);
    read(prevCustom->times, file);
  }

  fclose(file);
  return true;
}


/* -------------------------------   Delete   --------------------------------

  If the layout has been saved prompt for confirmation before deleting.
  Returns true if the layout was deleted.

*/

bool Times::Delete(cLayout& lo)
{
  CustomTimes *list, *prev;

  for (prev = list = head; list; list = list->next)
  {
    if (list->layout == lo)
      break;
    prev = list;
  }
  if (list == NULL)
    return false;

  char buf[80];
  sprintf(buf, "Are you sure you want to delete %s?", lo.str());
  if (MessageBox(buf, "Confirm", MB_YES | MB_NO).Run() == MB_NO)
    return false;

  if (prevCustom == list)
    prevCustom = NULL;
  if (list == head)
    head = list->next;
  else
    prev->next = list->next;
  delete list;

  return true;
}


/* ----------------------------   TimesWindow	----------------------------- */

static bool ignore_first;		// Ignore initial level event
static int  old_ID;

static const char* const level_str[] = {
  "Beginner", "Intermediate", "Expert", "Custom" };
static const char* const game_str[Records][Tables] = {
  { "Clear",     "Clear (auto-mark)",     "Clear (auto-open)" },
  { "Perimeter", "Perimeter (auto-mark)", "Perimeter (auto-open)" },
};

static FILE* OpenTimes(const char*);
static void  CloseTimes(FILE*);

enum
{
  ID_PERIMETER = 30004,
  ID_AUTOMARK,
  ID_AUTOOPEN,

  ID_CLEAR,
  ID_CLEARLEVEL,
  ID_CLEARGAME,
  ID_CLEARALL,

  ID_SAVEBEST
};


DEFINE_RESPONSE_TABLE(TimesWindow, TabWin)
  E_BUTTONUP(ID_OK, cmOK)

  E_RADIO_GROUP( ID_SCORETIME, ID_SCORESTATS, cmTable)

  E_BOXCHECKED(  ID_PERIMETER, cmTable)
  E_BOXCHECKED(  ID_AUTOMARK,  CheckAutoMark)
  E_BOXCHECKED(  ID_AUTOOPEN,  CheckAutoOpen)
  E_BOXUNCHECKED(ID_PERIMETER, cmTable)
  E_BOXUNCHECKED(ID_AUTOMARK,  cmTable)
  E_BOXUNCHECKED(ID_AUTOOPEN,  cmTable)

  E_RADIO_GROUP( BEGINNER, CUSTOM, RadioLevel)

  E_BUTTONUP(ID_CLEAR,	    cmClear)
  E_BUTTONUP(ID_CLEARLEVEL, cmClearLevel)
  E_BUTTONUP(ID_CLEARGAME,  cmClearGame)
  E_BUTTONUP(ID_CLEARALL,   cmClearAll)

  E_BUTTONUP(ID_SAVEBEST,   cmSaveBest)
END_RESPONSE_TABLE


#define TIMES_WIDTH  (69*FixedFont->width)
#define TIMES_HEIGHT (25*FixedFont->height + FWIDTH*2*2)

// Create the times' display and display table.
TimesWindow::TimesWindow(const Table& table, int lev, unsigned record) :
  TabWin(NULL, NULL, (ws.GetDeskWidth()  - TIMES_WIDTH)  / 2,
		     (ws.GetDeskHeight() - TIMES_HEIGHT) / 2,
		     (ws.GetDeskWidth()  + TIMES_WIDTH)  / 2 - 1,
		     (ws.GetDeskHeight() + TIMES_HEIGHT) / 2 - 1,
	 WA_CAPTION | WA_VISABLE | WA_BORDER | WA_SAVEAREA),
  CustomSave(field->Custom)
{
  const int fw = FixedFont->width,
	    fh = FixedFont->height > TIMESICONH ? FixedFont->height
						: TIMESICONH;

  const char* title;
  int score;
  highlight[0] = record & 0xffff;
  highlight[1] = record >> 16;
  highlev = lev;
  highauto = isAutoMark + isAutoOpen*2;
  if (highlight[0] < Entries && highlight[1] < Entries)
  {
    title = "New Record Time & 3BV/s!";
    score = ID_SCORETIME;
  }
  else if (highlight[0] < Entries)
  {
    title = "New Record Time!";
    score = ID_SCORETIME;
  }
  else if (highlight[1] < Entries)
  {
    title = "New Record 3BV/s!";
    score = ID_SCORE3BVPS;
  }
  else
  {
    score = ID_SCORETIME + -record-1;
    title = (char*)(score == ID_SCORETIME  ? "Best Times" :
		    score == ID_SCORE3BVPS ? "Best 3BV/s"
					   : "Statistics");
    highlev = 0;
  }
  //SetTitle(title);	// doesn't actually exist!
  Title = (char*)title;

  SysFont = FixedFont;
  SysFontBold = FixedFontBold;

  int ox  = 2*4*FWIDTH,
      oy  = CAPTION_HEIGHT + 5*FWIDTH,
      wid = 65*fw;

  header = new StaticText(this, "", ox, oy, wid);
  oy += FixedFont->height+8;
  ix = 57*fw;
  iy = oy;
  for (unsigned j = 0; j < Entries; j++)
  {
    fast[j] = new StaticText(this, "", ox, oy, wid);
    oy += fh+2;
  }
  status = new StaticText(this, "", ox, oy + fh, wid);

  ignore_first = true;
  old_ID = -1;

  wid = 13*fw+fw/2;
  ox  = fw;
  oy += 2*fh + 3*FWIDTH + 6*FWIDTH;
  int hyt = 3 + 3*(fh+1) + 3,
      oy1 = oy;

  #define XYWH ox, oy, ox+wid-1, oy+hyt-1

  scores = new RadioGroup(this, XYWH, RADIO_TYPE_DIP);
  scores->AddButton("&Time",       ID_SCORETIME);
  scores->AddButton("&3BV/s",      ID_SCORE3BVPS);
  scores->AddButton("&Statistics", ID_SCORESTATS);
  scores->SelectButton(score);

  ox += wid + fw+fw/2 + 3;
  oy += 3;
  wid = 12*fw+fw/2 - 20;
  hyt -= 3;
  perimeter = new CheckBox(this, ID_PERIMETER, "&Perimeter", ox, oy, wid,
				 Start == PERIMETER);
  oy += fh+1;
  automark = new CheckBox(this,ID_AUTOMARK,"Auto&Mark", ox,oy,wid, isAutoMark);
  oy += fh+1;
  autoopen = new CheckBox(this,ID_AUTOOPEN,"Auto&Open", ox,oy,wid, isAutoOpen);

  ox += -3 + wid+20 + fw+fw/2;
  oy = oy1;
  wid = 15*fw;
  hyt += fh+1 + 3;
  level = new RadioGroup(this, XYWH, RADIO_TYPE_DIP);
  level->AddButton("&Beginner",     BEGINNER);
  level->AddButton("&Intermediate", INTERMEDIATE);
  level->AddButton("&Expert",       EXPERT);
  // If there are no custom games, don't display anything, since there's no
  // disabled option.
  if (field->CustomPopup.number != 0)
  {
    level->AddButton("&Custom", CUSTOM);
    mx = ox + 16 + fw;
    my = oy + 3 + 4*(fh+1);
  }
  level->SelectButton(lev);

  wid = 13*fw;
  hyt = fh+4;
  ox = TIMES_WIDTH - fw - wid;
  clear = new TextButton(this, ID_CLEAR, XYWH, "Clear");
  oy += hyt + -2;
  clearlevel = new TextButton(this, ID_CLEARLEVEL, XYWH, "Clear Level");
  oy += hyt + -2;
  cleargame = new TextButton(this, ID_CLEARGAME, XYWH, "Clear Game");
  oy += hyt + -2;
  clearall = new TextButton(this, ID_CLEARALL, XYWH, "Clear All");

  oy += hyt + 2;
  save = new TextButton(this, ID_SAVEBEST, XYWH, "Save Best");

  wid = StringWidth("OK", FixedFont)*2;
  hyt = FixedFontBold->height+4;
  ox = (TIMES_WIDTH-wid)/2;
  oy = TIMES_HEIGHT - fw - hyt;
  ok = new TextButton(this, ID_OK, XYWH, "O&K");

  AddTabStop(scores);
  AddTabStop(perimeter);
  AddTabStop(automark);
  AddTabStop(autoopen);
  AddTabStop(level);
  AddTabStop(ok);

  Paint();
  RefreshWindow();
  Print(table);
  ignore_first = false;

  SysFont = SysFont1;
  SysFontBold = SysFontBold1;
}


TimesWindow::~TimesWindow()
{
  Title = NULL;
  for (unsigned j = 0; j < Entries; j++)
    delete fast[j];
  delete header; delete status;
  delete scores; delete level;
  delete perimeter; delete automark; delete autoopen;
  delete clear; delete clearlevel; delete cleargame; delete clearall;
  delete save;
  delete ok;
  field->Custom = CustomSave;
}


/* ----------------------------   PaintWindow	-----------------------------

  Draw the frames around the times and options.

*/

void TimesWindow::PaintWindow(int x1, int y1, int x2, int y2)
{
  Window::PaintWindow(x1, y1, x2, y2);

  ViewBuffer* buf = GetSubBuffer(x1, y1, x2, y2);
  const int   fw  = FixedFont->width,
	      fh  = FixedFont->height;

  int ox = 4*FWIDTH,
      oy = CAPTION_HEIGHT + 3*FWIDTH,
      hyt = 2*FWIDTH + fh+8 + Entries*(fh+2) + 2*fh + 3*FWIDTH;
  Frame(buf, ox, oy, TIMES_WIDTH-ox-1, oy+hyt-1);

  ox += 13*fw+fw/2 + fw+fw/2;
  oy += hyt + 6*FWIDTH;
  int wid = 12*fw+fw/2;
  hyt = 3 + 3*(fh+1) + 3;
  InvFrame(buf, ox, oy, ox+wid-1, oy+hyt-1);
     Frame(buf, ox+1, oy+1, ox+wid-2, oy+hyt-2);

  DeleteBuffer(buf);
}


/* -------------------------------   Print   --------------------------------

  Update the window with the values from table.

*/

static char* numdash(unsigned num)
{
  static char buf[8];
  if (num == 0)
  {
    buf[0] = '-';
    buf[1] = '\0';
  }
  else
    sprintf(buf, "%u", num);
  return buf;
}

static int numlen(unsigned num)
{
  if (num < 10)   return 1;
  if (num < 100)  return 2;
  if (num < 1000) return 3;
  return 3 + numlen(num / 1000);
}

void TimesWindow::Print(const Table& table)
{
  static const char* const heading[2] =
    { "   Time    Speed    3BV/s   3BV   Rating",
      "  3BV/s    Speed     Time   3BV   Rating" };

  int	lev = level->GetSelectedButton();
  int score = scores->GetSelectedButton();

  header->Paint();
  if (score == ID_SCORESTATS)
  {
    Title = (char*)"Statistics";
    char temp[Entries][80];
    for (unsigned j = 0; j < Entries; j++)
      *temp[j] = '\0';
    int len, len1;
    const char* title;
    if (table.gamestotal == 0)
      strcpy(temp[5], "                         No games played.");
    else
    {
      unsigned pc = unsigned(100. * table.games / table.gamestotal + 0.5);
      len = numlen(MAX(pc, table.gamestotal));
      len1 = numlen(MAX(table.longestwins, -table.longestloss));
      int spc = 16-len-len1;
      if (table.one)
      sprintf(temp[0], "   One-click wonders: %*u", len, table.one);
      sprintf(temp[1], "           Games won: %*u"
		       "%*cLongest winning streak: %*s",
		       len, table.games,
		       spc, ' ', len1, numdash(table.longestwins));
      sprintf(temp[2], "        Games played: %*u"
		       "%*cLongest losing  streak: %*s",
		       len, table.gamestotal,
		       spc, ' ', len1, numdash(-table.longestloss));
      int cur;
      if (table.current < 0)
      {
	title = "losing ";
	cur = -table.current;
      }
      else
      {
	title = "winning";
	cur = table.current;
      }
      sprintf(temp[3], "               Ratio: %*d%%"
		       "%*cCurrent %s streak: %*d",
		       len, pc, spc-1, ' ', title, len1, cur);

      if (table.games == 0)
      {
	strcpy( temp[5], "                       3BV   Rating");
	sprintf(temp[6], "%20s: %s",
			 (table.gamestotal == 1) ? "Game played" : "Average",
			 table.str(lev, score, 4));
      }
      else
      {
	const char *title1, *title2 = NULL;
	int i = 6;
	sprintf(temp[5], "                      %s", heading[0]);
	if (table.games > Entries)
	{
	  char tmp0[80], *tmp1;
	  strcpy(tmp0,	table.str(lev, ID_SCORETIME, 2));
		 tmp1 = table.str(lev, ID_SCORE3BVPS, 2);
	  if (strcmp(tmp0, tmp1) == 0)
	  {
	    sprintf(temp[i++], "        Average best: %s", tmp0);
	  }
	  else
	  {
	    sprintf(temp[i++], "   Average best time: %s", tmp0);
	    sprintf(temp[i++], "  Average best 3BV/s: %s", tmp1);
	  }
	  if (table.games == table.gamestotal)
	  {
	    title1 = "Average overall";
	  }
	  else
	  {
	    title1 = "Average winning";
	    title2 = "Average overall";
	  }
	}
	else if (table.games == table.gamestotal)
	{
	  title1 = (table.games == 1) ? "Game played" : "Average";
	}
	else if (table.games == 1)
	{
	  title1 = "Game won";
	  title2 = "Average";
	}
	else
	{
	  title1 = "Average winning";
	  title2 = "Average overall";
	}
	sprintf(temp[i], "%20s: %s", title1, table.str(lev, score, 1));
	if (title2)
	  sprintf(temp[i+1], "%20s: %s", title2, table.str(lev, score, 3));
      }
    }
    header->RefreshWindow();
    for (unsigned j = 0; j < Entries; j++)
    {
      if (j == 5 && table.gamestotal != 0)
      {
	if (*fast[5]->GetText())
	  fast[5]->SetText("");
	else
	  fast[5]->Paint();
	WriteText(fast[5]->GetBuffer(), 0,0,HighLightColour,FixedFont, temp[5]);
	fast[5]->RefreshWindow();
      }
      else
	fast[j]->SetText(temp[j]);
    }
    status->SetText("");
  }
  else
  {
    Title = (char*)(score == ID_SCORETIME ? "Best Times" : "Best 3BV/s");
    if (table.games == 0)
    {
      header->RefreshWindow();
      for (unsigned j = 0; j < Entries; j++)
      {
	if (j == 5)
	  fast[5]->SetText("                           No games won.");
	else
	  fast[j]->SetText("");
      }
      status->SetText("");
    }
    else
    {
      WriteText(header->GetBuffer(), 0, 0, HighLightColour, FixedFont,
		heading[score == ID_SCORE3BVPS]);
      header->RefreshWindow();
      for (unsigned j = 0; j < Entries; j++)
      {
	const Table::Entry& ent = table.fastest[score == ID_SCORE3BVPS][j];
	fast[j]->SetText(ent.str(lev, score, false));
	if (*ent.date != '\0')
	{
	  int i, y = iy + j * (FixedFont->height + 2);
#define IX(o)	   ix+(o)*(TIMESICONW+4)
#define IC	   0, 0, TIMESICONW-1, TIMESICONH-1
#define ICON(o, i) BitBlt(wnd, IX(o), y, TimesIcon##i, IC)
	  unsigned o = ent.options;
	  if (ent.start == PERIMETER)
	    i = 1;
	  else
	  {
	    i = 0;
	    ICON(0, Start[ent.start]);
	    if (o & OWrap)
	      BitBltTransparent(wnd, IX(0), y, TimesIconWrap, IC);
	  }
	  ICON(1, Finish[ent.finish]);
	  ICON(2, Counter[o & OCounter]);
	  ICON(3, General[bool(o & OMarks)][bool(o & OTrack)]);
	  ICON(4, Clear[bool(o & OClear)]);
	  RefreshWindow(absx+IX(i), absy+y,
			absx+IX(Options), absy+y+TIMESICONH-1);
	}
      }
      status->SetText(table.str(lev, score));
    }
    unsigned hl = highlight[score == ID_SCORE3BVPS];
    if (hl < Entries && highlev == lev &&
	highauto == automark->isChecked() + autoopen->isChecked()*2)
    {
      // 010115: Something I've done between 2.00 and 2.10 requires this.
      ws.CheckForEvents();
      StaticText* which = fast[hl];
      WriteText(which->GetBuffer(), 0, 0, HighLightColour, FixedFont,
		which->GetText());
      which->RefreshWindow();
    }
  }
  if (highlev == 0)
  {
    PaintWindow(0, 0, w, FWIDTH+SysFont->height);
    RefreshWindow(absx, absy, absx+w, absy+FWIDTH+SysFont->height);
  }
}


void TimesWindow::Run()
{
  SysFont = FixedFont;
  SysFontBold = FixedFontBold;
  SetFocus(ok);
  CatchMouse();
  CatchKeys();
  ws.RunEvents();
  ws.ResetEvents();
  ReleaseMouse();
  ReleaseKeys();
  SysFont = SysFont1;
  SysFontBold = SysFontBold1;
}


// Since there's no automatic opening AND marking, uncheck the other one if
// it's checked.
void TimesWindow::CheckAutoMark()
{
  if (autoopen->isChecked())
    autoopen->UnCheck();
  cmTable();
}

void TimesWindow::CheckAutoOpen()
{
  if (automark->isChecked())
    automark->UnCheck();
  cmTable();
}


// For custom games pop up a menu with the stored layouts.  If the menu is
// cancelled restore the previous level.
void TimesWindow::RadioLevel(int ID)
{
  if (old_ID == -1)			// Ignore the first call, from when
  {					//  the window is created.
    old_ID = ID;
    return;
  }

  if (ID == CUSTOM)
  {
    if (CustomPopupMenu(NULL, field->CustomPopup, absx+mx, absy+my).Run())
    {
      cmTable();
      old_ID = CUSTOM;
    }
    else if (old_ID != CUSTOM)
      level->SelectButton(old_ID);

    CatchKeys();			// It seems PopupMenu releases keys
  }
  else
  {
    old_ID = ID;
    cmTable();
  }
}


/* ------------------------------   CmTable   -------------------------------

  Find the table corresponding to the selections and display it.

*/

void TimesWindow::cmTable(int)
{
  Table* table = field->times.FindTable(level->GetSelectedButton(),
					perimeter->isChecked(),
					automark->isChecked(),
					autoopen->isChecked());
  Print(*table);
}


/* ------------------------------   CmClear   -------------------------------

  Clear the times currently displayed.

*/

void TimesWindow::cmClear()
{
  int lev   = level->GetSelectedButton(),
      peri  = perimeter->isChecked(),
      amark = automark->isChecked(),
      aopen = autoopen->isChecked();

  Table* table = field->times.FindTable(lev, peri, amark, aopen);
  if (!table->played())
    return;

  if (MessageBox("Are you sure you want to clear these records?", "Confirm",
		 MB_YES | MB_NO).Run() == MB_NO)
    return;

  if (table->won())
  {
    char buf[80];
    const char* desc = game_str[peri][aopen*2 + amark];
    if (lev == CUSTOM)
      sprintf(buf, "%s (%s) %s", level_str[3], field->Custom.str(), desc);
    else
      sprintf(buf, "%s %s", level_str[lev-BEGINNER], desc);
    FILE* file = OpenTimes(buf);
    table->save(file, NULL, lev);
    CloseTimes(file);
  }
  table->clear();
  Print(*table);
}


/* ----------------------------   CmClearLevel   ----------------------------

  Clear all the times associated with the current level.

*/


// Return true if any games have been played for this level; set won to true
// if any games have been completed.
static bool played(const TTable& table, bool& won)
{
  bool any;
  any = won = false;
  for (int r = 0; !won && r < Records; ++r)
  {
    for (int j = 0; j < Tables; ++j)
    {
      if (table[r][j].played())
      {
	any = true;
	if (table[r][j].won())
	{
	  won = true;
	  break;
	}
      }
    }
  }
  return any;
}


void TimesWindow::cmClearLevel()
{
  int lev = level->GetSelectedButton();
  TTable& table = *(TTable*)field->times.FindTable(lev, 0, 0, 0);
  bool won;
  if (!played(table, won))
    return;

  char buf[80];
  sprintf(buf, "Are you sure you want to clear all the %s records?",
	       level_str[lev - BEGINNER]);
  if (MessageBox(buf, "Confirm", MB_YES | MB_NO).Run() == MB_NO)
    return;

  if (won)
  {
    if (lev == CUSTOM)
      sprintf(buf, "%s (%s)", level_str[3], field->Custom.str());
    else
      strcpy(buf, level_str[lev - BEGINNER]);
    FILE* file = OpenTimes(buf);
    for (int r = 0; r < Records; ++r)
      for (int j = 0; j < Tables; ++j)
	table[r][j].save(file, game_str[r][j], lev);
    CloseTimes(file);
  }

  for (int r = 0; r < Records; ++r)
    for (int j = 0; j < Tables; ++j)
      table[r][j].clear();

  Print(table[0][0]);
}


/* ----------------------------   CmClearGame   -----------------------------

  Clear all the times associated with the current game type.

*/

void TimesWindow::cmClearGame()
{
  int r   = perimeter->isChecked();
  int tbl = autoopen->isChecked()*2 + automark->isChecked();

  bool any = false;
  any |= field->times.Beginner[r][tbl].played();
  any |= field->times.Intermediate[r][tbl].played();
  any |= field->times.Expert[r][tbl].played();
  if (!any)
  {
    for (Times::CustomTimes* list = field->times.head; list; list = list->next)
    {
      if (list->times[r][tbl].played())
      {
	any = true;
	break;
      }
    }
  }
  if (!any)
    return;

  char buf[80];
  sprintf(buf, "Are you sure you want to clear all the %s times?",
	       game_str[r][tbl]);
  if (MessageBox(buf, "Confirm", MB_YES | MB_NO).Run() == MB_NO)
    return;

  any = false;
  any |= field->times.Beginner[r][tbl].won();
  any |= field->times.Intermediate[r][tbl].won();
  any |= field->times.Expert[r][tbl].won();
  if (!any)
  {
    for (Times::CustomTimes* list = field->times.head; list; list = list->next)
    {
      if (list->times[r][tbl].won())
      {
	any = true;
	break;
      }
    }
  }

  FILE* file = (any) ? OpenTimes(game_str[r][tbl]) : NULL;

  field->times.Beginner    [r][tbl].save(file, level_str[0], BEGINNER);
  field->times.Intermediate[r][tbl].save(file, level_str[1], INTERMEDIATE);
  field->times.Expert	   [r][tbl].save(file, level_str[2], EXPERT);

  field->times.Beginner    [r][tbl].clear();
  field->times.Intermediate[r][tbl].clear();
  field->times.Expert	   [r][tbl].clear();

  for (Times::CustomTimes* list = field->times.head; list; list = list->next)
  {
    sprintf(buf, "%s (%s)", level_str[3], list->layout.str());
    field->Custom = list->layout;
    list->times[r][tbl].save(file, buf, CUSTOM);
    list->times[r][tbl].clear();
  }

  CloseTimes(file);

  Print(field->times.Beginner[r][tbl]);
}


/* -----------------------------   CmClearAll   -----------------------------

  Clear all times of the field.

*/

static void clear_level(FILE* file, TTable& table, const char* title, int lev)
{
  bool won;
  if (!played(table, won))
    return;

  if (file != NULL && won)
  {
    int k = fprintf(file, "%s\n", title);
    for (; k > 1; --k)
      fputc('-', file);
    fputs("\n\n", file);
  }

  for (int r = 0; r < Records; ++r)
  {
    for (int j = 0; j < Tables; ++j)
    {
      table[r][j].save(file, game_str[r][j], lev);
      table[r][j].clear();
    }
  }
}


void TimesWindow::cmClearAll()
{
  if (MessageBox("Are you sure you want to clear ALL times?", "Confirm",
		 MB_YES | MB_NO).Run() == MB_NO)
    return;

  FILE* file = OpenTimes("");

  clear_level(file, field->times.Beginner,     level_str[0], BEGINNER);
  clear_level(file, field->times.Intermediate, level_str[1], INTERMEDIATE);
  clear_level(file, field->times.Expert,       level_str[2], EXPERT);

  for (Times::CustomTimes* list = field->times.head; list; list = list->next)
  {
    char buf[80];
    sprintf(buf, "%s (%s)", level_str[3], list->layout.str());
    field->Custom = list->layout;
    clear_level(file, list->times, buf, CUSTOM);
  }

  CloseTimes(file);
  Print(field->times.Beginner[0][0]);
}


/* -----------------------------   CmSaveBest	-----------------------------

  Save all the best times of the field.

*/

static void save_best(FILE* file, TTable& table, const char* title, int lev)
{
  bool won = false;
  played(table, won);
  if (!won)
    return;

  int j = fprintf(file, "%s\n", title);
  for (; j > 1; --j)
    putc('-', file);
  fputs("\n\n", file);

  for (int r = 0; r < Records; ++r)
    for (j = 0; j < Tables; ++j)
      table[r][j].best(file, game_str[r][j], lev);
}


void TimesWindow::cmSaveBest()
{
  FILE* file = OpenTimes(NULL);
  if (file == NULL)
    return;

  save_best(file, field->times.Beginner,     level_str[0], BEGINNER);
  save_best(file, field->times.Intermediate, level_str[1], INTERMEDIATE);
  save_best(file, field->times.Expert,	     level_str[2], EXPERT);

  for (Times::CustomTimes* list = field->times.head; list; list = list->next)
  {
    char buf[80];
    sprintf(buf, "%s (%s)", level_str[3], list->layout.str());
    field->Custom = list->layout;
    save_best(file, list->times, buf, CUSTOM);
  }

  CloseTimes(file);
}


static FILE* OpenTimes(const char* title)
{
  FILE* file = fopen((title == NULL) ? "best.txt" : "times.txt", "a");
  if (file != NULL)
  {
    time_t now;
    char buf[80];
    time(&now);
    strftime(buf, sizeof(buf), "%e %B, %Y", localtime(&now));
    const char* type = (field == squarefield)  ? "Square" :
		       (field == recthexfield) ? "Hexagon" :
						 "Hexagonal";
    if (title == NULL)
      title = "best";
    int k = fprintf(file, "%s field %s%stimes as at %s\n", type, title,
			  (*title) ? " " : "", buf);
    for (; k > 1; --k)
      fputc('=', file);
    fputs("\n\n", file);
  }
  return file;
}


static void CloseTimes(FILE* file)
{
  if (file != NULL)
  {
    for (int j = 0; j < 78; ++j)
      putc('-', file);
    fputs("\n\n\n", file);
    fclose(file);
  }
}
